/**
 * Copyright (c) 2006-2015, JGraph Ltd
 * Copyright (c) 2006-2015, Gaudenz Alder
 */
/**
 * Class: mxOutline
 *
 * Implements an outline (aka overview) for a graph. Set <updateOnPan> to true
 * to enable updates while the source graph is panning.
 *
 * Example:
 *
 * (code)
 * var outline = new mxOutline(graph, div);
 * (end)
 *
 * If an outline is used in an <mxWindow> in IE8 standards mode, the following
 * code makes sure that the shadow filter is not inherited and that any
 * transparent elements in the graph do not show the page background, but the
 * background of the graph container.
 *
 * (code)
 * if (document.documentMode == 8)
 * {
 *   container.style.filter = 'progid:DXImageTransform.Microsoft.alpha(opacity=100)';
 * }
 * (end)
 *
 * To move the graph to the top, left corner the following code can be used.
 *
 * (code)
 * var scale = graph.view.scale;
 * var bounds = graph.getGraphBounds();
 * graph.view.setTranslate(-bounds.x / scale, -bounds.y / scale);
 * (end)
 *
 * To toggle the suspended mode, the following can be used.
 *
 * (code)
 * outline.suspended = !outln.suspended;
 * if (!outline.suspended)
 * {
 *   outline.update(true);
 * }
 * (end)
 *
 * Constructor: mxOutline
 *
 * Constructs a new outline for the specified graph inside the given
 * container.
 *
 * Parameters:
 *
 * source - <mxGraph> to create the outline for.
 * container - DOM node that will contain the outline.
 */
function mxOutline(source, container) {
  this.source = source;

  if (container != null) {
    this.init(container);
  }
}

/**
 * Function: source
 *
 * Reference to the source <mxGraph>.
 */
mxOutline.prototype.source = null;

/**
 * Function: outline
 *
 * Reference to the <mxGraph> that renders the outline.
 */
mxOutline.prototype.outline = null;

/**
 * Function: graphRenderHint
 *
 * Renderhint to be used for the outline graph. Default is faster.
 */
mxOutline.prototype.graphRenderHint = mxConstants.RENDERING_HINT_FASTER;

/**
 * Variable: enabled
 *
 * Specifies if events are handled. Default is true.
 */
mxOutline.prototype.enabled = true;

/**
 * Variable: showViewport
 *
 * Specifies a viewport rectangle should be shown. Default is true.
 */
mxOutline.prototype.showViewport = true;

/**
 * Variable: border
 *
 * Border to be added at the bottom and right. Default is 10.
 */
mxOutline.prototype.border = 10;

/**
 * Variable: enabled
 *
 * Specifies the size of the sizer handler. Default is 8.
 */
mxOutline.prototype.sizerSize = 8;

/**
 * Variable: labelsVisible
 *
 * Specifies if labels should be visible in the outline. Default is false.
 */
mxOutline.prototype.labelsVisible = false;

/**
 * Variable: updateOnPan
 *
 * Specifies if <update> should be called for <mxEvent.PAN> in the source
 * graph. Default is false.
 */
mxOutline.prototype.updateOnPan = false;

/**
 * Variable: sizerImage
 *
 * Optional <mxImage> to be used for the sizer. Default is null.
 */
mxOutline.prototype.sizerImage = null;

/**
 * Variable: minScale
 *
 * Minimum scale to be used. Default is 0.0001.
 */
mxOutline.prototype.minScale = 0.0001;

/**
 * Variable: suspended
 *
 * Optional boolean flag to suspend updates. Default is false. This flag will
 * also suspend repaints of the outline. To toggle this switch, use the
 * following code.
 *
 * (code)
 * nav.suspended = !nav.suspended;
 *
 * if (!nav.suspended)
 * {
 *   nav.update(true);
 * }
 * (end)
 */
mxOutline.prototype.suspended = false;

/**
 * Variable: forceVmlHandles
 *
 * Specifies if VML should be used to render the handles in this control. This
 * is true for IE8 standards mode and false for all other browsers and modes.
 * This is a workaround for rendering issues of HTML elements over elements
 * with filters in IE 8 standards mode.
 */
mxOutline.prototype.forceVmlHandles = document.documentMode == 8;

/**
 * Function: createGraph
 *
 * Creates the <mxGraph> used in the outline.
 */
mxOutline.prototype.createGraph = function (container) {
  var graph = new mxGraph(
    container,
    this.source.getModel(),
    this.graphRenderHint,
    this.source.getStylesheet()
  );
  graph.foldingEnabled = false;
  graph.autoScroll = false;

  return graph;
};

/**
 * Function: init
 *
 * Initializes the outline inside the given container.
 */
mxOutline.prototype.init = function (container) {
  this.outline = this.createGraph(container);

  // Do not repaint when suspended
  var outlineGraphModelChanged = this.outline.graphModelChanged;
  this.outline.graphModelChanged = mxUtils.bind(this, function (changes) {
    if (!this.suspended && this.outline != null) {
      outlineGraphModelChanged.apply(this.outline, arguments);
    }
  });

  // Enables faster painting in SVG
  if (mxClient.IS_SVG) {
    var node = this.outline.getView().getCanvas().parentNode;
    node.setAttribute("shape-rendering", "optimizeSpeed");
    node.setAttribute("image-rendering", "optimizeSpeed");
  }

  // Hides cursors and labels
  this.outline.labelsVisible = this.labelsVisible;
  this.outline.setEnabled(false);

  this.updateHandler = mxUtils.bind(this, function (sender, evt) {
    if (!this.suspended && !this.active) {
      this.update();
    }
  });

  // Updates the scale of the outline after a change of the main graph
  this.source.getModel().addListener(mxEvent.CHANGE, this.updateHandler);
  this.outline.addMouseListener(this);

  // Adds listeners to keep the outline in sync with the source graph
  var view = this.source.getView();
  view.addListener(mxEvent.SCALE, this.updateHandler);
  view.addListener(mxEvent.TRANSLATE, this.updateHandler);
  view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.updateHandler);
  view.addListener(mxEvent.DOWN, this.updateHandler);
  view.addListener(mxEvent.UP, this.updateHandler);

  // Updates blue rectangle on scroll
  mxEvent.addListener(this.source.container, "scroll", this.updateHandler);

  this.panHandler = mxUtils.bind(this, function (sender) {
    if (this.updateOnPan) {
      this.updateHandler.apply(this, arguments);
    }
  });
  this.source.addListener(mxEvent.PAN, this.panHandler);

  // Refreshes the graph in the outline after a refresh of the main graph
  this.refreshHandler = mxUtils.bind(this, function (sender) {
    this.outline.setStylesheet(this.source.getStylesheet());
    this.outline.refresh();
  });
  this.source.addListener(mxEvent.REFRESH, this.refreshHandler);

  // Creates the blue rectangle for the viewport
  this.bounds = new mxRectangle(0, 0, 0, 0);
  this.selectionBorder = new mxRectangleShape(
    this.bounds,
    null,
    mxConstants.OUTLINE_COLOR,
    mxConstants.OUTLINE_STROKEWIDTH
  );
  this.selectionBorder.dialect = this.outline.dialect;

  if (this.forceVmlHandles) {
    this.selectionBorder.isHtmlAllowed = function () {
      return false;
    };
  }

  this.selectionBorder.init(this.outline.getView().getOverlayPane());

  // Handles event by catching the initial pointer start and then listening to the
  // complete gesture on the event target. This is needed because all the events
  // are routed via the initial element even if that element is removed from the
  // DOM, which happens when we repaint the selection border and zoom handles.
  var handler = mxUtils.bind(this, function (evt) {
    var t = mxEvent.getSource(evt);

    var redirect = mxUtils.bind(this, function (evt) {
      this.outline.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt));
    });

    var redirect2 = mxUtils.bind(this, function (evt) {
      mxEvent.removeGestureListeners(t, null, redirect, redirect2);
      this.outline.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
    });

    mxEvent.addGestureListeners(t, null, redirect, redirect2);
    this.outline.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
  });

  mxEvent.addGestureListeners(this.selectionBorder.node, handler);

  // Creates a small blue rectangle for sizing (sizer handle)
  this.sizer = this.createSizer();

  if (this.forceVmlHandles) {
    this.sizer.isHtmlAllowed = function () {
      return false;
    };
  }

  this.sizer.init(this.outline.getView().getOverlayPane());

  if (this.enabled) {
    this.sizer.node.style.cursor = "nwse-resize";
  }

  mxEvent.addGestureListeners(this.sizer.node, handler);

  this.selectionBorder.node.style.display = this.showViewport ? "" : "none";
  this.sizer.node.style.display = this.selectionBorder.node.style.display;
  this.selectionBorder.node.style.cursor = "move";

  this.update(false);
};

/**
 * Function: isEnabled
 *
 * Returns true if events are handled. This implementation
 * returns <enabled>.
 */
mxOutline.prototype.isEnabled = function () {
  return this.enabled;
};

/**
 * Function: setEnabled
 *
 * Enables or disables event handling. This implementation
 * updates <enabled>.
 *
 * Parameters:
 *
 * value - Boolean that specifies the new enabled state.
 */
mxOutline.prototype.setEnabled = function (value) {
  this.enabled = value;
};

/**
 * Function: setZoomEnabled
 *
 * Enables or disables the zoom handling by showing or hiding the respective
 * handle.
 *
 * Parameters:
 *
 * value - Boolean that specifies the new enabled state.
 */
mxOutline.prototype.setZoomEnabled = function (value) {
  this.sizer.node.style.visibility = value ? "visible" : "hidden";
};

/**
 * Function: refresh
 *
 * Invokes <update> and revalidate the outline. This method is deprecated.
 */
mxOutline.prototype.refresh = function () {
  this.update(true);
};

/**
 * Function: createSizer
 *
 * Creates the shape used as the sizer.
 */
mxOutline.prototype.createSizer = function () {
  if (this.sizerImage != null) {
    var sizer = new mxImageShape(
      new mxRectangle(0, 0, this.sizerImage.width, this.sizerImage.height),
      this.sizerImage.src
    );
    sizer.dialect = this.outline.dialect;

    return sizer;
  } else {
    var sizer = new mxRectangleShape(
      new mxRectangle(0, 0, this.sizerSize, this.sizerSize),
      mxConstants.OUTLINE_HANDLE_FILLCOLOR,
      mxConstants.OUTLINE_HANDLE_STROKECOLOR
    );
    sizer.dialect = this.outline.dialect;

    return sizer;
  }
};

/**
 * Function: getSourceContainerSize
 *
 * Returns the size of the source container.
 */
mxOutline.prototype.getSourceContainerSize = function () {
  return new mxRectangle(
    0,
    0,
    this.source.container.scrollWidth,
    this.source.container.scrollHeight
  );
};

/**
 * Function: getOutlineOffset
 *
 * Returns the offset for drawing the outline graph.
 */
mxOutline.prototype.getOutlineOffset = function (scale) {
  return null;
};

/**
 * Function: getOutlineOffset
 *
 * Returns the offset for drawing the outline graph.
 */
mxOutline.prototype.getSourceGraphBounds = function () {
  return this.source.getGraphBounds();
};

/**
 * Function: update
 *
 * Updates the outline.
 */
mxOutline.prototype.update = function (revalidate) {
  if (
    this.source != null &&
    this.source.container != null &&
    this.outline != null &&
    this.outline.container != null
  ) {
    var sourceScale = this.source.view.scale;
    var scaledGraphBounds = this.getSourceGraphBounds();
    var unscaledGraphBounds = new mxRectangle(
      scaledGraphBounds.x / sourceScale + this.source.panDx,
      scaledGraphBounds.y / sourceScale + this.source.panDy,
      scaledGraphBounds.width / sourceScale,
      scaledGraphBounds.height / sourceScale
    );

    var unscaledFinderBounds = new mxRectangle(
      0,
      0,
      this.source.container.clientWidth / sourceScale,
      this.source.container.clientHeight / sourceScale
    );

    var union = unscaledGraphBounds.clone();
    union.add(unscaledFinderBounds);

    // Zooms to the scrollable area if that is bigger than the graph
    var size = this.getSourceContainerSize();
    var completeWidth = Math.max(size.width / sourceScale, union.width);
    var completeHeight = Math.max(size.height / sourceScale, union.height);

    var availableWidth = Math.max(
      0,
      this.outline.container.clientWidth - this.border
    );
    var availableHeight = Math.max(
      0,
      this.outline.container.clientHeight - this.border
    );

    var outlineScale = Math.min(
      availableWidth / completeWidth,
      availableHeight / completeHeight
    );
    var scale = isNaN(outlineScale)
      ? this.minScale
      : Math.max(this.minScale, outlineScale);

    if (scale > 0) {
      if (this.outline.getView().scale != scale) {
        this.outline.getView().scale = scale;
        revalidate = true;
      }

      var navView = this.outline.getView();

      if (navView.currentRoot != this.source.getView().currentRoot) {
        navView.setCurrentRoot(this.source.getView().currentRoot);
      }

      var t = this.source.view.translate;
      var tx = t.x + this.source.panDx;
      var ty = t.y + this.source.panDy;

      var off = this.getOutlineOffset(scale);

      if (off != null) {
        tx += off.x;
        ty += off.y;
      }

      if (unscaledGraphBounds.x < 0) {
        tx = tx - unscaledGraphBounds.x;
      }
      if (unscaledGraphBounds.y < 0) {
        ty = ty - unscaledGraphBounds.y;
      }

      if (navView.translate.x != tx || navView.translate.y != ty) {
        navView.translate.x = tx;
        navView.translate.y = ty;
        revalidate = true;
      }

      // Prepares local variables for computations
      var t2 = navView.translate;
      scale = this.source.getView().scale;
      var scale2 = scale / navView.scale;
      var scale3 = 1.0 / navView.scale;
      var container = this.source.container;

      // Updates the bounds of the viewrect in the navigation
      this.bounds = new mxRectangle(
        (t2.x - t.x - this.source.panDx) / scale3,
        (t2.y - t.y - this.source.panDy) / scale3,
        container.clientWidth / scale2,
        container.clientHeight / scale2
      );

      // Adds the scrollbar offset to the finder
      this.bounds.x +=
        (this.source.container.scrollLeft * navView.scale) / scale;
      this.bounds.y +=
        (this.source.container.scrollTop * navView.scale) / scale;

      var b = this.selectionBorder.bounds;

      if (
        b.x != this.bounds.x ||
        b.y != this.bounds.y ||
        b.width != this.bounds.width ||
        b.height != this.bounds.height
      ) {
        this.selectionBorder.bounds = this.bounds;
        this.selectionBorder.redraw();
      }

      // Updates the bounds of the zoom handle at the bottom right
      var b = this.sizer.bounds;
      var b2 = new mxRectangle(
        this.bounds.x + this.bounds.width - b.width / 2,
        this.bounds.y + this.bounds.height - b.height / 2,
        b.width,
        b.height
      );

      if (
        b.x != b2.x ||
        b.y != b2.y ||
        b.width != b2.width ||
        b.height != b2.height
      ) {
        this.sizer.bounds = b2;

        // Avoids update of visibility in redraw for VML
        if (this.sizer.node.style.visibility != "hidden") {
          this.sizer.redraw();
        }
      }

      if (revalidate) {
        this.outline.view.revalidate();
      }
    }
  }
};

/**
 * Function: mouseDown
 *
 * Handles the event by starting a translation or zoom.
 */
mxOutline.prototype.mouseDown = function (sender, me) {
  if (this.enabled && this.showViewport) {
    var tol = !mxEvent.isMouseEvent(me.getEvent()) ? this.source.tolerance : 0;
    var hit =
      this.source.allowHandleBoundsCheck && (mxClient.IS_IE || tol > 0)
        ? new mxRectangle(
            me.getGraphX() - tol,
            me.getGraphY() - tol,
            2 * tol,
            2 * tol
          )
        : null;
    this.zoom =
      me.isSource(this.sizer) ||
      (hit != null && mxUtils.intersects(shape.bounds, hit));
    this.startX = me.getX();
    this.startY = me.getY();
    this.active = true;

    if (
      this.source.useScrollbarsForPanning &&
      mxUtils.hasScrollbars(this.source.container)
    ) {
      this.dx0 = this.source.container.scrollLeft;
      this.dy0 = this.source.container.scrollTop;
    } else {
      this.dx0 = 0;
      this.dy0 = 0;
    }
  }

  me.consume();
};

/**
 * Function: mouseMove
 *
 * Handles the event by previewing the viewrect in <graph> and updating the
 * rectangle that represents the viewrect in the outline.
 */
mxOutline.prototype.mouseMove = function (sender, me) {
  if (this.active) {
    this.selectionBorder.node.style.display = this.showViewport ? "" : "none";
    this.sizer.node.style.display = this.selectionBorder.node.style.display;

    var delta = this.getTranslateForEvent(me);
    var dx = delta.x;
    var dy = delta.y;
    var bounds = null;

    if (!this.zoom) {
      // Previews the panning on the source graph
      var scale = this.outline.getView().scale;
      bounds = new mxRectangle(
        this.bounds.x + dx,
        this.bounds.y + dy,
        this.bounds.width,
        this.bounds.height
      );
      this.selectionBorder.bounds = bounds;
      this.selectionBorder.redraw();
      dx /= scale;
      dx *= this.source.getView().scale;
      dy /= scale;
      dy *= this.source.getView().scale;
      this.source.panGraph(-dx - this.dx0, -dy - this.dy0);
    } else {
      // Does *not* preview zooming on the source graph
      var container = this.source.container;
      var viewRatio = container.clientWidth / container.clientHeight;
      dy = dx / viewRatio;
      bounds = new mxRectangle(
        this.bounds.x,
        this.bounds.y,
        Math.max(1, this.bounds.width + dx),
        Math.max(1, this.bounds.height + dy)
      );
      this.selectionBorder.bounds = bounds;
      this.selectionBorder.redraw();
    }

    // Updates the zoom handle
    var b = this.sizer.bounds;
    this.sizer.bounds = new mxRectangle(
      bounds.x + bounds.width - b.width / 2,
      bounds.y + bounds.height - b.height / 2,
      b.width,
      b.height
    );

    // Avoids update of visibility in redraw for VML
    if (this.sizer.node.style.visibility != "hidden") {
      this.sizer.redraw();
    }

    me.consume();
  }
};

/**
 * Function: getTranslateForEvent
 *
 * Gets the translate for the given mouse event. Here is an example to limit
 * the outline to stay within positive coordinates:
 *
 * (code)
 * outline.getTranslateForEvent = function(me)
 * {
 *   var pt = new mxPoint(me.getX() - this.startX, me.getY() - this.startY);
 *
 *   if (!this.zoom)
 *   {
 *     var tr = this.source.view.translate;
 *     pt.x = Math.max(tr.x * this.outline.view.scale, pt.x);
 *     pt.y = Math.max(tr.y * this.outline.view.scale, pt.y);
 *   }
 *
 *   return pt;
 * };
 * (end)
 */
mxOutline.prototype.getTranslateForEvent = function (me) {
  return new mxPoint(me.getX() - this.startX, me.getY() - this.startY);
};

/**
 * Function: mouseUp
 *
 * Handles the event by applying the translation or zoom to <graph>.
 */
mxOutline.prototype.mouseUp = function (sender, me) {
  if (this.active) {
    var delta = this.getTranslateForEvent(me);
    var dx = delta.x;
    var dy = delta.y;

    if (Math.abs(dx) > 0 || Math.abs(dy) > 0) {
      if (!this.zoom) {
        // Applies the new translation if the source
        // has no scrollbars
        if (
          !this.source.useScrollbarsForPanning ||
          !mxUtils.hasScrollbars(this.source.container)
        ) {
          this.source.panGraph(0, 0);
          dx /= this.outline.getView().scale;
          dy /= this.outline.getView().scale;
          var t = this.source.getView().translate;
          this.source.getView().setTranslate(t.x - dx, t.y - dy);
        }
      } else {
        // Applies the new zoom
        var w = this.selectionBorder.bounds.width;
        var scale = this.source.getView().scale;
        this.source.zoomTo(
          Math.max(this.minScale, scale - (dx * scale) / w),
          false
        );
      }

      this.update();
      me.consume();
    }

    // Resets the state of the handler
    this.index = null;
    this.active = false;
  }
};

/**
 * Function: destroy
 *
 * Destroy this outline and removes all listeners from <source>.
 */
mxOutline.prototype.destroy = function () {
  if (this.source != null) {
    this.source.removeListener(this.panHandler);
    this.source.removeListener(this.refreshHandler);
    this.source.getModel().removeListener(this.updateHandler);
    this.source.getView().removeListener(this.updateHandler);
    mxEvent.removeListener(this.source.container, "scroll", this.updateHandler);
    this.source = null;
  }

  if (this.outline != null) {
    this.outline.removeMouseListener(this);
    this.outline.destroy();
    this.outline = null;
  }

  if (this.selectionBorder != null) {
    this.selectionBorder.destroy();
    this.selectionBorder = null;
  }

  if (this.sizer != null) {
    this.sizer.destroy();
    this.sizer = null;
  }
};
